/**
 * 
 */
package org.stars.daostars.core.parser;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.stars.dao.exception.parser.CircularDependencyException;
import org.stars.daostars.core.SqlQuery;
import org.stars.daostars.exceptions.parser.QueryNotFoundException;
import org.stars.daostars.sqlmapper.SqlMapper;
import org.stars.util.StringUtility;


/**
 * Cerca tutte le subquery incluse all'interno delle query e sostituisce il
 * relativo place holder con il codice sql associato.
 * <p>
 * Il place holder per le query incluse è <code>=[NOME_QUERY]</code>.
 * <p>
 * 
 * @author Francesco Benincasa (908099)
 * 
 */
public abstract class FindSubQuery {

	/**
	 * Matcher compilato.
	 */
	private static Pattern pattern = Pattern.compile(getRegExMatcher(), Pattern.MULTILINE);

	/**
	 * Parsa tutte le query contenute nella definizione di dao per sistemare i
	 * nomi delle query incluse in =[<NOME QUERY>], togliendo tutti gli spazi di
	 * troppo all'interno della definizione del place holder.
	 * 
	 * @param definition
	 * @throws Exception
	 */
	private static void findPlaceHolder(SqlMapper definition) throws Exception {
		for (SqlQuery query : definition.getQuerySet().values()) {
			List<String> usedQuery = new ArrayList<String>();
			String sql = query.getPlainSql();
			sql = findPlaceHolder(sql, usedQuery);

			query.setPlainSql(sql);
			query.setUsedQueryNameSet(usedQuery);
		}
	}

	/**
	 * Analizza la query e sostituisce i place holder all'interno di un codice
	 * sql. In output si ottiene anche l'elenco dei nomi delle query usate.
	 * 
	 * @param sql
	 *            sql in input
	 * @param usedQuery
	 *            elenco delle query usate. (OUTPUT)
	 * @return stringa elaborata
	 * @throws Exception
	 *             in caso di errore
	 */
	private static String findPlaceHolder(String sql, List<String> usedQuery) throws Exception {

		Matcher matcher = pattern.matcher(sql);
		StringBuffer sb = new StringBuffer();
		String sTemp;

		while (matcher.find()) {
			sTemp = "";

			if (matcher.group(GROUP_INCLUDE_STATEMENT) != null) {
				sTemp = matcher.group(GROUP_INCLUDE_STATEMENT);
				sTemp = sTemp.toUpperCase();
				usedQuery.add(sTemp);
			}
			matcher.appendReplacement(sb, "=[" + sTemp + "]");

		}

		matcher.appendTail(sb);

		return sb.toString();
	}

	/**
	 * Processa tutte le query della definizione di un Dao per definire
	 * completamente le varie query mediante l'inserimento in testa del codice
	 * sql semplice (non elaborato) della query padre.
	 * 
	 * @param dao
	 *            definizione del dao da processare.
	 * @return <code>true</code> se va tutto bene.
	 * @throws Exception
	 *             eccezzione
	 */
	private static boolean replacePlaceHolder(SqlMapper dao) throws Exception {
		Stack<String> stack = new Stack<String>(); // pila dei dao analizzati.
		Set<String> processedQuery = new TreeSet<String>();

		for (SqlQuery sqlQuery : dao.getQuerySet().values()) {
			if (!processedQuery.contains(sqlQuery.getName())) {
				stack.push(sqlQuery.getName());
				replacePlaceHolder(stack, sqlQuery, dao, processedQuery);
				stack.pop();
			}
		}

		return true;
	}

	/**
	 * Metodo usato per cercare ricorsivamente le definizioni delle varie query
	 * e di risolvere ricorsivamente le dipendenze tra le varie query.
	 * <p>
	 * Per evitare di entrare in loop infiniti, è stata introdotto lo stack, in
	 * modo da poter sempre avere sottocontrollo le query attualmente
	 * analizzate.
	 * 
	 * @param stack
	 *            stack delle query attualmente analizzate.
	 * @param sqlQuery
	 *            oggetto DaoSqlQuery da analizzare.
	 * @param dao
	 *            definizione del dao attualmente in analisi.
	 * @param processedQueries
	 *            definizione delle query gi� processate.
	 * @return <code>true</code>.
	 * @throws Exception
	 *             Se qualcosa va male, viene sollevata un'eccezione.
	 */
	private static boolean replacePlaceHolder(Stack<String> stack, SqlQuery sqlQuery, SqlMapper dao,
			Set<String> processedQueries) throws Exception {

		if (sqlQuery.getUsedQueryNameSet().size() == 0) {
			processedQueries.add(sqlQuery.getName());
			return true;
		}

		String plainSql = sqlQuery.getPlainSql();
		for (String usedQueryName : sqlQuery.getUsedQueryNameSet()) {

			SqlQuery usedQuery = dao.getQuery(usedQueryName);

			if (usedQuery == null) {
				String msg = "Dao " + dao.getName() + ", query " + sqlQuery.getName()
						+ ": It's impossible to include the query " + usedQueryName;
				Exception e = new QueryNotFoundException(msg);
				throw (e);
			}

			if (!processedQueries.contains(usedQueryName)) {
				if (stack.contains(usedQueryName)) {
					String msg = "Dao " + dao.getName() + ", query " + sqlQuery.getName()
							+ ": Circular dependency for the query " + usedQueryName;

					throw (new CircularDependencyException(msg));
				}

				stack.push(usedQueryName);
				// cerchiamo di processare le query usate, se non processate
				replacePlaceHolder(stack, usedQuery, dao, processedQueries);

				stack.pop();
			}

			String usedQuerySql = usedQuery.getPlainSql();
			plainSql = StringUtility.replace(plainSql, "=[" + usedQuery.getName() + "]", usedQuerySql);
		}
		sqlQuery.setPlainSql(plainSql);

		return true;
	}

	/**
	 * @return pattern da cercare
	 */
	public static String getRegExMatcher() {
		return regIncludeQuery;
	}

	/**
	 * parser
	 */
	public final static String regNome = "[\\w\\.\\-_]+";

	/**
	 * reg ex per le query incluse
	 */
	public final static String regIncludeQuery = "((\\=)\\[\\s*(\\s*(" + regNome + ")\\s*)(\\((\\s*(" + regNome
			+ ")\\s*)\\))?\\s*\\])";

	/**
	 * group include statement
	 */
	public static int GROUP_INCLUDE_STATEMENT = 4;

	/**
	 * Cerca tutte le query incluse all'interno delle query e sostituisce il
	 * relativo place holder con il codice sql associato.
	 * <p>
	 * Il place holder per le query incluse è <code>=[NOME_QUERY]</code>.
	 * <p>
	 * 
	 * @param definition
	 * @return <code>true</code> in caso non ci sono errori
	 * @throws Exception
	 */
	public static boolean execute(SqlMapper definition) throws Exception {
		boolean bRet = true;

		findPlaceHolder(definition);
		replacePlaceHolder(definition);

		return bRet;
	}
}
